﻿//
// Created by LiuYou on 2021/4/13.
//

#include <stdio.h>

/**
 * @brief 头文件和库文件

 头文件是指在C/C++编程中，可以写在程序里面，以#include 的形式包含的文件。
 对于C语言，一般以.h作为扩展名；对于C++,可以没有扩展名，也可以以.h或者.hpp做扩展名。规范中对头文件的扩展名并没有做约定。
 库文件是将一系列的源文件进行编译打包，形成的二进制文件包，其中封装着函数接口，在编程中可以由其它源文件调用。库文件可分为静态库和动态库两种。
    二者的区别为：
        1 头文件是文本文件，是可供阅读的；库文件是二进制文件，不可直接阅读。
        2 头文件在预处理时使用；库文件在链接中使用。
        3 头文件中是函数或定义的声明，及少量内联函数的使用，一般不包含非静态函数实现；库文件中包含函数的实现。
        4 头文件是手动编写的，库文件是编译生成的。
    二者也有联系：
        1 虽然没有规定调用库函数必须引用头文件，但一般约定，当调用库函数的时候需要引用接口函数所在的头文件。
        2 一个库文件均有一组头文件与其对应。
        3 库文件和对应头文件的作者应为同一个。
        4 作者必须保证头文件中所声明的函数类型及参数列表，与库文件中的实现是相同的。

    头文件中有函数的申明，库文件实现函数的定义。
    比如，printf函数。使用时应包括stdio.h，打开stdio.h你只能看到，printf这
    个函数的申明,却看不到printf具体是怎么实现的，而函数的实现在相应的C库
    中。而库文件一般是以二进制形式而不是C源文件形式提供给用户使用的。程序
    中包括了stdio.h这个头文件。链接器就能根据头件中的信息找到printf这个函
    数的实现并链接进这个程序代码段里。
    总结起来就是，库文件通过头文件向外导出接口。用户通过头文件找到库文件中
    函数实现的代码从而把这段代码链接到用户程序中去。
    头文件可视，库文件不可视。

 从编程的角度讲，通常有静态库文件和动态库文件。windows静态库文件就是.lib文件，动态库文件就是.dll文件。
 内容一样，都是将函数封装在一起编译后供自己或他人调用。好处在于编译后的库文件看不到源代码，可保密；同时不会因为不小心修改了函数而出问题，便于维护。
 两种库的区别在于静态库被调用时直接加载到内存，而动态库再是在需要的时候加载到内存，不使用的时候再从内存释放。
 unix系统的静态库文件和动态库文件后缀分别是.a和.so
 头文件可以使用代码库文件里的函数。

 以上内容来自网络。在这里留攸向他们表示由衷感谢！
 */
void headerFileAndLibraryFile() {

}

/**
 * @brief C库
 * 访问C库
 *      如何访问C库取决于实现，因此你要了解当前系统的一般情况。
 *      首先，可以在多个不同的位置找到库函数。例: getchar()函数通常作为宏定义在stdio.h头文件中，
 *      而strlen()函数通常在库文件中。
 *      其次，不同的系统搜索这些函数的方法不同。下面介绍3中可能的方法:
 *          1> 自动访问
 *              在一些系统中，只需编译程序，就可以使用一些常用的库函数。
 *              记住，在使用函数之前必须先声明函数的类型，通过包含合适的头文件即可完成。
 *              在描述库函数的用户手册中，会指出使用某函数时应包含哪个头文件。
 *          2> 文件包含
 *              如果函数被定义为宏，那么可以通过#include指令包含定义宏函数的文件。通常，类似的宏都放在了合适名称的头文件中。
 *          3> 库包含
 *              在编译或链接程序的某些阶段，可能需要指定库选项。即使在自动检查标准库的系统中，也会有不常用的函数库。
 *              此时必须通过编译时选项显式指定这些库。注意: 这个过程与包含头文件不同。头文件提供函数原型，而库选项
 *              告诉系统到哪里查找函数代码。
 *
 * 查看库描述(其实就是查阅头文件或者对库文件中函数的描述文档)
 *      了解函数文档。
 *      阅读文档的关键是看懂函数原型。许多内容随时间变化而变化。
 *
 * 数学库
 *      数学库中包含许多有用的数学函数。math.h头文件提供这些函数的原型。
 *
 * 通用工具库(utility library)
 *      通用工具库包含各种函数，包括随机数生成器、查找和排序函数、转换函数和内存管理函数。
 *      这些函数的函数原型都在stdlib.h头文件中。
 *      exit()
 *      atexit()
 *
 *      // quick sort(快速排序)
 *      qsort()
 *      快速排序算法在C语言中是qsort()，其函数原型:
 *          void qsort(void* base, size_t nmemb, size_t size, int (*compar)(const void* , const void*));
 *      base: 指向待排序数组的首元素。
 *      nmemb: 待排序项的数量。
 *      size: 显式指明待排序数组中每个元素的大小。如果排序double数组，那么第三个参数应该是sizeof(double)。
 *      compar: 指向函数的指针。这个被指针指向的比较函数用于确定排序的顺序。
 *              该比较函数的参数，如果第一个参数的值大于第二个，比较函数返回正数;
 *                              如果第一个参数的值小于第二个，则返回负数。
 *
 * 断言库(assertion library)
 *      assert.h头文件支持的断言库是一个用于辅助调试程序的小型库。它由assert()宏组成。
 *      使用assert()有好处: 它不仅能自动标识文件和出问题的行号，还有一种无需更改代码就能开启或关闭assert()的机制。
 *      如果认为已经排除了程序的BUG，就可以把下面的宏定义写在包含assert.h的位置前面: #define NDEBUG 并重新编译程序，
 *      这样编译器就会禁用文件中的所有assert()语句。如果程序又出现问题，可以注释这条#define指令，然后重新编译程序，
 *      这样就重新启用了assert()语句。
 *
 * string.h库中的memcpy()和memmove()
 *      不能把一个数组赋给另一个数组，所以要通过循环把数组中的每个元素赋给另一个数组相应的元素。
 *      有一个例外情况是: 使用strcpy()和strncpy()函数来处理字符数组。
 *      而memcpy()和memmove()函数可以处理任意类型的数组。
 *          void* memcpy(void* restrict s1, const void* restrict s2, size_t n);
 *          void* memmove(void* s1, const void* s2, size_t n);
 *          这两个函数都从s2指向的位置拷贝n字节到s1指向的位置，而且都返回s1的值。
 *          所不同的是: memcpy()的参数带关键字restrict，即memcpy()假设两个内存区域之间没有重叠;
 *                      (理解: 例如一个数组a，有11个元素，如果memcpy的时候把从a【0】开始的6个元素拷贝到a【5】的地址，就会出现重叠，
 *                      即a【5】这个地址既是源数据的地址，也是目的地址。这么拷贝容易出错，因为一开始就会把a【0】的值填到a【5】上，
 *                      a【5】就不再是原来的值了，之后会把a【5】的值填到a【10】上，这就不对了。)
 *                      而memmove()不作这样的假设，所以拷贝过程类似于先把所有字节拷贝到一个临时缓冲区，然后再拷贝到最终目的地。
 *                      如果使用memcpy()时，两个区域重叠会怎样？其行为是未定义的，这意味着该函数可能正常工作，也可能失败。所以
 *                      在使用memcpy()函数时有责任确保两个区域不重叠。
 *
 * 可变参数: stdarg.h
 *      《C Primer Plus》 P560
 *
 * C预处理器和C库是C语言的两个重要的附件。C预处理器遵循预处理器指令，在编译源代码之前调整源代码。C库提供许多有助于完成各种任务的函数，
 * 例如: 输入输出、文件处理、内存管理、排序搜索、数学运算、字符串处理等。


 个人比较倾向于头文件里面尽量不include其他头文件，而是在cpp里面去include。
 一般情况下，尽量把能放到源文件cpp的代码都放到源文件里。这一点待会详细说。
 特殊情况下，比如完全template的header－only generic code，可以考虑建两个头文件。声明（declarations）放到第一个头文件，实现（implementation detail）放到第二个头文件里：前者＃include后者。

 先说第一点：尽量把能放到cpp的code放到cpp里。先从编译器角度说一点，头文件中声明的static变量等，要记得在源文件中定义。如果有头文件中直接定义的函数，记得mark inline，否则如果有多个translation unit同时引用时，会产生multiple definitions的错误（违背了One Definition Rule）。
 下面从软件库开发的角度，讲一下自己的理解。很多时候，对于你的项目，库，或者产品，encapsulation是极为重要的一点。从这个方面考虑，头文件中应该只能有public APIs，其他所有的属于implementation detail的东西都应当放在源文件里面。一个项目的递交经常是public APIs + compiled library （for linking）。这样的话，有若干优点
 从开发者的角度讲：1。保护源代码（正所谓encapsulation）2。不必要的信息没有必要展示给用户
 从用户的角度讲：1。用户大多数情况下仅需查看API，头文件掺杂实现代码会显得繁琐不宜查询。2。很重要的一点：当开发者更新库的实现后，用户只需更新，然后重新link即可，不必重新编译。
 从产品或者库的质量角度讲：头文件，源文件区分得当的库，编译时间+运行程序体积（binary size）会比纯粹头文件的库优化很多。这一点有时也很重要：1。设想用你的库开发的项目，每次更改都需费长时间编译，对开发效率和应用体验都不好；2。特别对于嵌入式项目，运行程序体积往往有一定限制，太大的binary甚至很可能不会cache friendly，影响运行效率。
 综上，原则上应该把头文件和源文件尽量区分开。从encapsulation方面讲：源代码保护，API清晰化；从编译时间+开发使用讲：减少无用的重新编译，方便用户更新（这也是为何pimpl idiom被广泛使用的一个原因，reference：The Joy of Pimpls (or, More About the Compiler-Firewall Idiom)）

 那么inline或者template怎么办？
 template从编译器角度考虑，template的实现似乎只能放在同文件中，放在cpp中会出现linker error。可是还是有技巧可以将template的实现放到cpp中：在源文件对template argument类型完全特化（explicit specialization，具体见：Standard C++）。
 － 这一点，如果从开发库的广泛型（genericity）角度考虑，可能完全不合逻辑；但是对于交付给用户应用程序的特殊库版本，这么做是会有一定收获的。具体来讲，如果对于用户应用程序，数据类型是固定的，那么可以完全提供一个库版本，其template argument在cpp中完全specialize化：这样对于应用程序的角度，收获就是前面提到的几点。
 －对于库的开发者，template的实现必然要放在头文件中，但是，还是有技巧将实现分离于用户的public APIs的：详见第二点。inlineinline函数（非static）若果定义不在头文件中，会产生linker error。一个特例是，如果确定此函数只会被一个translation unit引用，则可以将其放到cpp中，不过这种情况较少见，也不推荐。同template一样，有同样的技巧，可以将inline函数的实现分离于用户的public APIs：详见第二点。
 再说第二点：header中隐藏implementation detail很多header only library注重运行时间的效率（runtime efficiency），泛化（genericity），因此广泛使用template，并给用户对参数类型选择的灵活性。在这种template library里面，可以用多个头文件的方式将代码实现和APIs分开。
 具体的做法就是，创建一个头文件（比如 xx.hh），仅包含API；再用另一个头文件封装代码实现（比如 xx.hxx）。前者因用后者，就实现了头文件源文件的分离。这种做法在STL，boost等库里都是很常见的。开发者通常将doc写在第一个头文件中，第二个头文件被当作源代码，用doxygen产生doc时滤掉第二个头文件的后缀名，这样只有第一个头文件的public APIs进入documentation，从而产生清晰可读的文档。
 同样的方法也适用于inline函数。如有分析不到的地方，还请大家指正！
 链接：https://www.zhihu.com/question/51789910/answer/127603777

 */
void library() {

}

/**
 * @brief 预处理器
 *
 * 翻译程序的第一步
 *      1> 在预处理之前，编译器必须对该程序进行一些翻译处理。
 *      首先，编译器把源代码中出现的字符映射到源字符集。该工程中处理多字节字符和三字符序列——字符扩展让C更加国际化。
 *      2> 编译器定位每个反斜杠后面跟着换行符的实例，并删除它们。也就是说，把下面两个物理行:
 *          printf("That's wond\
 *                  erful!\n");
 *      转换成一个逻辑行:
 *          printf("That's wonderful!\n");
 *      3> 编译器把文本划分成预处理记号序列、空白序列和注释序列(记号是由空格、制表符或换行符分隔的项)。
 *      这里注意的是: 编译器将用一个空格字符替代每一条注释。因此，下面的代码:
 *          int注释fox;
 *          将变成
 *          int fox;
 *      而且，实现可以用一个空格替换所有的空白字符序列(不包括换行符)。
 *      4> 最后，程序已经准备好进入预处理阶段，预处理器查找一行中以#号开始的预处理指令。
 *
 * #define
 *      #指令 可以出现在源文件的任何地方，其定义从指令出现的地方到文件末尾有效。
 *
 *      通过#define指令来定义符号常量(各种各样！P522)。
 *
 *      记号(P525)
 *
 *      在#define中使用参数
 *          在#define中使用参数可以创建外形和作用于函数类似的类函数宏。
 *          例:
 *              #define CALCULATE(X, Y) ((X)+(Y)/2)
 *
 *          注意: 不要在宏中使用递增或递减运算符。
 *
 *      变参宏: ...和__VA_ARGS__
 *          通过把宏参数列表中最后的参数(最后一个参数)写成省略号来实现变参宏。这样，预定义宏__VA_ARGS__可用于替换部分中，表明省略号代表什么。
 *          例:
 *              #define PRINT(...) printf(__VA_ARGS__)
 *
 *              #define PRINT(X, ...) printf("#X" __VA_ARGS__)
 *
 *      宏和函数的选择(各有优缺点)
 *      如果打算使用宏来加快程序的运行速度，那么首先要确定使用宏和使用函数是否会导致较大的差异。
 *      在程序中只使用一次和宏无法明显减少程序的运行时间。在嵌套循环中使用宏更有助于提高效率。许多系统提供程序分析器以帮助程序员压缩程序中最耗时的部分。
 *      假设你开发了一些方便的宏函数，是否每写一个新程序都要重写这些宏？如果使用#include指令就不用这样做了。
 *      使用宏时，用圆括号把宏的参数和整个替换体括起来，这样能避免宏展开后的正确性。
 *
 * #include
 *      头文件中最常用的形式如下:
 *          - 符号常量 例: EOF、NULL等
 *          - 宏函数
 *          - 函数原型
 *          - 结构体定义 例: FILE
 *          - 类型定义 例: 使用#define和typedef
 *          - 声明外部变量供其它文件共享(P534这里有点难理解！！！！)
 *              例: 如果已经开发了共享某个变量的一系列函数，该变量报告某种状况(如，错误情况)，这种方法就很有效。
 *              这种情况下，可以在包含这些函数声明的源代码文件定义一个文件作用域的外部链接变量。
 *                  int status = 0;
 *              然后，可以在与源代码文件相关联的头文件中进行引用式声明:
 *                  extern int status;
 *
 * 其它指令
 *      #undef指令取消之前的#define定义。
 *      #line指令用于重置行和文件信息。
 *      #error指令用于给出错误消息
 *      #pragma指令用于向编译器发出指令。
 *
 *      例:
 *          #define LIMIT 400
 *          // ...
 *          #undef LIMIT
 *          #define宏的作用域从它在文件中的声明处开始，知道用#undef指令取消宏为止，或延伸至文件尾。
 *          注意: 如果宏通过头文件引入，那么#define在文件中的位置取决于#include指令的位置。
 *
 *      条件编译(条件编译也是用法很多！！！！)
 *          #ifdef指令(和if-else很相似)
 *              #ifdef 宏
 *              // 如果#define 宏 ，则执行下面的指令
 *              // ...
 *              #else
 *              // 如果没有#define 宏，则执行下面的指令
 *              // ...
 *              #endif
 *
 *              也可以
 *              #ifdef 宏
 *              // ...
 *              #endif
 *
 *          #ifndef指令
 *              和#ifdef用法类似，但是可以用来进行头文件重复包含。
 *              例:
 *                  #ifndef _STDIO_H
 *                  #define _STDIO_H
 *                  // ...
 *                  #endif
 *                  我们也可以这样写。但是，由于标准库保留使用下划线作为前缀，所以在自己的代码中不要这样写。可以这样写
 *                  #ifndef NAME_H
 *                  #define NAME_H
 *                  // ...
 *                  #endif
 *
 *          #if和#elif指令
 *              #if指令很像C语言中的if。#if后面跟整形常量表达式。
 *              #if (...)
 *              // ...
 *              #endif
 *
 *              #if ()
 *              // ...
 *              #elif ()
 *              // ...
 *              #endif
 *
 *              #if ()
 *              // ...
 *              #elif ()
 *              // ...
 *              #elif ()
 *              // ...
 *              #else
 *              // ...
 *              #endif
 *
 *              #if ()
 *              // ...
 *              #else
 *              // ...
 *              #endif
 *
 * 条件编译可以让程序更容易移植。改变文件开头部分的几个关键的定义，即可根据不同的系统设置不同的值和包含不同的文件。
 *
 * 预定义宏(见本函数)
 * __func__(是C语言预定义标识符，而不是预定义宏)！！！！
 *
 * #line和#error
 *      #line指令重置__LINE__和__FILE__宏报告的行号和文件名。
 *      // 把当前行号重置为1000
 *      #line 1000
 *      // 把行号重置为10，把文件名重置为cool.c
 *      #line 10 "cool.c"
 *
 *      #error指令让预处理器发出一条错误消息，该消息包含指令中的文本。如果可能的话，编译过程应该中断。
 *      #if __STDC__VERSION__ != 201112L
 *      #error Not C11
 *      #endif
 *      我试了VS2019不支持上面三行的写法。
 *
 * #prama(编译指示)
 *      在现在的编译器中，可以通过命令行参数或IDE菜单修改编译器的一些设置。#prama把编译器指令放入源代码文件中。
 *
 * 宏必须定义为一条逻辑行，但是可以用 \ 把一条逻辑行分隔成多条物理行。
 *
 * 一般不在头文件中放置可执行代码。
 *
 */
void preprocessor() {
    printf("__DATE__: %s\n", __DATE__);
    printf("__FILE__: %s\n", __FILE__);
    printf("__LINE__: %d\n", __LINE__);
    printf("__TIME__: %s\n", __TIME__);
    printf("__func__: %s\n", __func__);
}


int main() {
    printf("__func__: %s\n", __func__);
    preprocessor();

    return 0;
}